import contextvars
import inspect
import os
import re
import xml
import sys
import pytz
import math
import time
import uuid
import types
import random
import socket
import loguru
import hashlib
import asyncio
import textwrap
import datetime
import binascii
import traceback
import itertools
import functools
import xmltodict
from starlette.concurrency import run_in_threadpool


def install_uvloop():
    """安装uvloop
    """

    try:
        import uvloop
    except ModuleNotFoundError:
        Utils.log.warning(f'uvloop is not supported (T＿T)')
    else:
        uvloop.install()
        Utils.log.success(f'uvloop {uvloop.__version__} installed')


class Utils:
    os = os
    log = loguru.logger
    sys = sys
    math = math
    uuid = uuid
    time = time
    random = random
    hashlib = hashlib
    asyncio = asyncio
    datetime = datetime
    textwrap = textwrap
    traceback = traceback
    itertools = itertools
    functools = functools

    @staticmethod
    def package_async_func(func, *args, **kwargs):
        """将普通函数或协程函数打包成协程函数供await等待
            当func为普通函数时，await时会放入线程池运行，以避免同步io对loop的阻塞
            当func为协程函数时，await时无差别
        """
        if asyncio.iscoroutinefunction(func):

            @functools.wraps(func)
            async def async_func():

                return await func(*args, **kwargs)
        else:
            @functools.wraps(func)
            async def async_func():

                return await run_in_threadpool(func, *args, **kwargs)

        return async_func

    @staticmethod
    def package_sync_func(func, *args, **kwargs):
        """将普通函数或协程函数打包成同步函数，加（）运行即实现异步
            当func为协程函数时，将允许同步方式启动并作为task放入loop调度
            当func为同步函数时，将被放入线程池运行，以避免同步io对loop的阻塞
        """
        if asyncio.iscoroutinefunction(func):
            @functools.wraps(func)
            def sync_func():
                asyncio.create_task(func(*args, **kwargs))
        else:
            @functools.wraps(func)
            def sync_func():
                asyncio.create_task(run_in_threadpool(func, *args, **kwargs))

        return sync_func

    @staticmethod
    def call_soon(func, *args, context=None, **kwargs):

        Utils.call_later(0, func, *args, context=context, **kwargs)

    @staticmethod
    def call_soon_threadsafe(func, *args, context=None, **kwargs):

        if context is None:
            context = contextvars.copy_context()

        asyncio.get_event_loop().call_soon_threadsafe(
            Utils.package_sync_func(func, *args, **kwargs),
            context=context
        )

    @staticmethod
    def call_later(delay, func, *args, context=None, **kwargs):

        if context is None:
            context = contextvars.copy_context()

        asyncio.get_event_loop().call_later(
            delay,
            Utils.package_sync_func(func, *args, **kwargs),
            context=context
        )

    @staticmethod
    def call_at(when, func, *args, context=None, **kwargs):

        if context is None:
            context = contextvars.copy_context()

        asyncio.get_event_loop().call_at(
            when,
            Utils.package_sync_func(func, *args, **kwargs),
            context=context
        )

    @staticmethod
    def run_until_complete(future):
        return asyncio.get_event_loop().run_until_complete(future)

    @staticmethod
    @types.coroutine
    def wait_frame(count=10):
        """暂停指定帧数
        """
        for _ in range(max(1, count)):
            yield

    @classmethod
    def ip2int(cls, val):

        try:
            return int(binascii.hexlify(socket.inet_aton(val)), 16)
        except socket.error:
            return int(binascii.hexlify(socket.inet_pton(socket.AF_INET6, val)), 16)

    @classmethod
    def int2ip(cls, val):

        try:
            return socket.inet_ntoa(binascii.unhexlify(r'%08x' % val))
        except socket.error:
            return socket.inet_ntop(socket.AF_INET6, binascii.unhexlify(r'%032x' % val))

    @classmethod
    def time2stamp(cls, time_str, format_type=r'%Y-%m-%d %H:%M:%S', timezone=None):

        if timezone is None:
            return int(datetime.datetime.strptime(time_str, format_type).timestamp())
        else:
            return int(
                datetime.datetime.strptime(time_str, format_type).replace(tzinfo=pytz.timezone(timezone)).timestamp())

    @classmethod
    def stamp2time(cls, time_int=None, format_type=r'%Y-%m-%d %H:%M:%S', timezone=None):

        if time_int is None:
            time_int = cls.timestamp()

        if timezone is None:
            return time.strftime(format_type, datetime.datetime.fromtimestamp(time_int).timetuple())
        else:
            return time.strftime(format_type,
                                 datetime.datetime.fromtimestamp(time_int, pytz.timezone(timezone)).timetuple())

    @staticmethod
    def datetime2time(datetime_time, format_type=r'%Y-%m-%d %H:%M:%S'):
        return time.strftime(format_type, datetime_time.timetuple())

    @classmethod
    def time2datetime(cls, time_str, format_type=r'%Y-%m-%d %H:%M:%S', timezone=None):
        timestamp = cls.time2stamp(time_str, format_type, timezone=timezone)

        if timezone:
            return datetime.datetime.fromtimestamp(timestamp, pytz.timezone(timezone))
        return datetime.datetime.fromtimestamp(timestamp)

    @staticmethod
    def timestamp():
        return int(time.time())

    @staticmethod
    def delta_datetime(
            origin=False,
            add=True,
            **kwargs
    ):
        if add:
            datetime_time = datetime.datetime.now() + datetime.timedelta(**kwargs)
        else:
            datetime_time = datetime.datetime.now() - datetime.timedelta(**kwargs)

        if origin:
            datetime_time = datetime_time.replace(hour=0, minute=0, second=0, microsecond=0)

        return datetime_time

    @staticmethod
    def loop_time():
        """获取当前loop时钟
        """
        loop = asyncio.events.get_event_loop()

        return loop.time()

    @staticmethod
    def format_time(format=r'%Y-%m-%d %H:%M:%S'):
        return time.strftime(format, datetime.datetime.now().timetuple())

    @classmethod
    def re_match(cls, pattern, value):

        result = re.match(pattern, value)

        return result if result else None

    @classmethod
    def xml_encode(cls, dict_val, root_tag=r'root'):

        xml_doc = xml.dom.minidom.Document()

        root_node = xml_doc.createElement(root_tag)
        xml_doc.appendChild(root_node)

        def _convert(_doc, _node, _dict):

            for key, val in _dict.items():

                temp = _doc.createElement(key)

                if isinstance(val, dict):
                    _convert(_doc, temp, val)
                else:
                    temp.appendChild(_doc.createTextNode(str(val)))

                _node.appendChild(temp)

        _convert(xml_doc, root_node, dict_val)

        return xml_doc

    @classmethod
    def xml_decode(cls, val: str):

        return xmltodict.parse(val)

    @classmethod
    def split_int(cls, val, sep=r',', minsplit=0, maxsplit=-1):

        result = [int(item.strip()) for item in val.split(sep, maxsplit) if item.strip().lstrip(r'-').isdigit()]

        fill = minsplit - len(result)

        if fill > 0:
            result.extend(0 for _ in range(fill))

        return result

    @classmethod
    def join(cls, *args, sep=','):
        return sep.join(str(i) for i in args)

    @classmethod
    def catch_error(cls, default=None, reraise=False, err_handler=None, log_level=None):
        """Please check 'ErrCatcher' for various parameters"""
        return ErrCatcher(
            default=default,
            reraise=reraise,
            err_handler=err_handler,
            log_level=log_level
        )


class ErrCatcher:
    """
    异常捕捉器
    Usage:
            the sync code
        >>> def sync_func():
        >>>     pass
        >>> with ErrCatcher():
        >>>     sync_func()
        or:
        >>> @ErrCatcher()
        >>> def sync_func():
        >>>     pass

            the async code
        >>> async def async_func():
        >>>     pass
        >>> async with ErrCatcher():
        >>>     await async_func()
        or:
        >>> @ErrCatcher()
        >>> async def async_func():
        >>>     pass
    """

    def __init__(self, default=None, reraise=False, err_handler=None, log_level=None):
        """
        :param default: If an exception occurs: it can return a default value for the function when it is used as a decorator
        :param reraise: If reraise is True, It will print exception log and continue to throw the exception out
        :param err_handler: it will be called when an exception occurs, you can pass a coroutine function or sync function
                            when 'err_handler' called, we will pass the exception value to it

                            1.if you use it in async code and the 'err_handler' is a coroutine function,
                            you should use it like this:
                                >>> async def async_func():
                                >>>     pass
                                >>> async with ErrCatcher():
                                >>>     await async_func()
                                or:
                                >>> @ErrCatcher()
                                >>> async def async_func():
                                >>>     pass
                            but if you use it in async code and the 'err_handler' is a sync function,
                            the 'err_handler' will be put into the thread pool for execution

                            2.if you use it in sync code and the 'err_handler' is a sync function,
                            you should use it like this:
                                >>> def sync_func():
                                >>>     pass
                                >>> with ErrCatcher():
                                >>>     sync_func()
                                or:
                                >>> @ErrCatcher()
                                >>> def sync_func():
                                >>>     pass
                            but if you use it in sync code and the 'err_handler' is a coroutine function,
                            the 'err_handler' will be executed asynchronously

                            Small details: if you want pass some arguments to 'err_handler', you can use functools.partial

        :param log_level: If an exception occurs: It determines the print level of the log based on the 'log_level'
                          you should pass 'warning'、 'info' ...
        """
        self._log_level = log_level
        self._err_handler = err_handler
        self._reraise = reraise
        self._default = default
        self._from_decorator = False

        assert callable(err_handler), '"err_handler" must be callable'

    def _err_log_print(self, exc_type, exc_val, exc_tb):

        if not self._log_level or not hasattr(loguru.logger, self._log_level.lower()):
            loguru.logger.exception(exc_val)
        else:
            getattr(loguru.logger, self._log_level.lower())(traceback.format_exc())

    def __enter__(self):

        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_val:
            self._err_log_print(exc_type, exc_val, exc_tb)
            if self._err_handler:
                if asyncio.iscoroutinefunction(self._err_handler):
                    Utils.call_soon(self._err_handler, exc_val)
                else:
                    self._err_handler(exc_val)

        return not self._reraise

    async def __aenter__(self):
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        if exc_val:
            self._err_log_print(exc_type, exc_val, exc_tb)
            if self._err_handler:
                if asyncio.iscoroutinefunction(self._err_handler):
                    await self._err_handler(exc_val)
                else:
                    await run_in_threadpool(self._err_handler, exc_val)

        return not self._reraise

    def __call__(self, func):
        self._from_decorator = True
        if asyncio.iscoroutinefunction(func):

            async def wrapper(*args, **kwargs):
                async with self:
                    return await func(*args, **kwargs)
                return self._default

        elif inspect.isgeneratorfunction(func):

            def wrapper(*args, **kwargs):
                with self:
                    return (yield from func(*args, **kwargs))
                return self._default

        else:

            def wrapper(*args, **kwargs):
                with self:
                    return func(*args, **kwargs)
                return self._default

        functools.update_wrapper(wrapper, func)
        return wrapper
